package detector.audio.processing;

import edu.washington.cs.sounddetector.server.api.object.*;
import java.util.*;

import android.app.Activity;
import android.content.Context;
import android.media.*;
import android.media.MediaRecorder.AudioSource;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.ProgressBar;

/**
 * Provides functionality for subscribing to "peak events", that is, you can set
 * it to call a method you define when the microphone audio goes above the
 * detection threshold (by using a PeakHandler). The monitor will only detect
 * events while it is listening (stop listening to save resources).
 * 
 * @author Kyle
 * 
 */

public class Monitor {
	private static final String TAG = "Monitor";
	// NOTE: Currently, cannot have multiple peakHandlers.
	/*
	 * Uses a singleton pattern and static factory to ensure only 1 monitor is
	 * ever instantiated (there is no need for multiple monitors)
	 */
	private static Monitor monitor;
	private static short maxValue = 0;
	private static Set<AudioListener> audioListeners;
	private PollThread pt;
	private PeakHandler ph;

	// Private
	private Monitor() {
		audioListeners = new HashSet<AudioListener>();
	}

	/**
	 * @return The monitor
	 */
	public static Monitor getMonitor() {
		if (monitor == null)
			monitor = new Monitor();
		return monitor;
	}

	/**
	 * Starts the monitor listening for peak events and calling PeakHandlers as
	 * peak events occur. If monitor is already listening, restarts it.
	 */
	public void startListening() {
		// Check if we are already listening
		if (isListening()) {
			// if so, stop
			stopListening();
		}
		// spawn the listening thread
		pt = new PollThread(this);
	}

	/**
	 * Stops listening for peak events and calling PeakHandlers.
	 */
	public void stopListening() {
		if (isListening()) {
			pt.close();
			// Wait for the thread to finish
			try {
				pt.join();
			} catch (InterruptedException e) {
				Log.e(TAG, Log.getStackTraceString(e));
			}
			pt = null;
		}
	}

	/**
	 * Sets the PeakHandle to be called, s.t. every time a peak event occurs,
	 * ph.peakDetected(...) will be called. If monitor is currently listening,
	 * will stop and restart the monitor after setting it up to use this
	 * peakHandler
	 * 
	 * @param ph
	 *            the PeakHandler to use when a peak event occurs
	 */
	public void setPeakHandler(PeakHandler ph) {
		if (isListening()) {
			stopListening();
			this.ph = ph;
			startListening();
		} else {
			this.ph = ph;
		}

	}

	/**
	 * Adds al to the list of audiolisteners to call every few milliseconds.
	 * Call removeAudioListener when done. Not guaranteed to work if Monitor is
	 * already listening
	 * 
	 * @param lh
	 */
	public void addAudioListener(AudioListener al) {
		audioListeners.add(al);
	}

	/**
	 * 
	 * @return true if it is listening for peak events
	 */
	public boolean isListening() {
		return pt != null;
	}

	/**
	 * 
	 * @return the PeakHandler this monitor is using, null if none
	 */
	public PeakHandler getPeakHandler() {
		return ph;
	}

	/**
	 * monitor behavior is unspecified if the returned set is modified
	 * 
	 * @return the set of audio listeners currently subscribed to the monitor.
	 * 
	 */
	public Set<AudioListener> getAudioListeners() {
		return audioListeners;
	}

}

/**
 * This thread continuously checks the audio input for peaks, and calls the
 * PeakHandler method when one occurs and the audio is finished recording. It
 * also calls the PeakHandler's onAudioReceived method and passes the volume
 * level every few milliseconds (specified by AUDIO_POLL_RATE).
 * 
 * @author Kyle
 * 
 */
final class PollThread extends Thread {
	// NOTE: AudioRecord has a bug where stop just releases it. You need to
	// re-initialize it every time you want to restart it

	private AudioRecord recorder = null;
	private static final short AUDIO_POLL_RATE = 2048; // how many samples
														// between each call to
														// onAudioReceived()?
	private static final int SAMPLE_RATE = 44100;
	private static final int SMOOTHING = 1800;
	private static final short THRESHOLD = 8;
	private static final int MIN_PEAK_SPACING_IN_SAMPLES = (int) (44100 * .25);
	private static final int RECORD_DURATION_IN_SAMPLES = (int) (44100 * 4); // How
																				// many
																				// samples
																				// to
																				// grab
																				// for
																				// each
																				// peak
																				// event
	private static final int PEAK_DETECT_DURATION_IN_SAMPLES = (int) (44100 * 5); // How
																					// long
																					// to
																					// look
																					// over
																					// to
																					// grab
																					// peak
																					// times
	private static final String TAG = "PollThread";
	private int threshold;
	private int samplesWrittenToAudioClip = 0; // stores the number of samples
												// recorded for current peak
												// event
	private short[] record; // stores the recorded audio data for the current
							// peak event
	private PeakHandler ph;
	private PollThreadState state;
	private short highestAudio = 0; // tracks the current highest audio level
	private short audioPollCounter = 0;
	private List<PeakEvent> peakTimes;
	private double startTime; // in seconds
	private double curTime;
	private int samplesProcessed; // number of samples processed since peak
									// detected
	public Handler mHandler;
	private short lastLPF = 0;

	// For each subpeakj
	private double subPeakTime; // time in seconds from the start of the
								// peakevent that the subpeak started
	private short subPeakVolume; // height of subpeak

	// NOTE: There are redundant calls to "close()" in this class

	private enum PollThreadState {
		STOPPED, RUNNING, INPEAK, INSUBPEAK,
	}

	/**
	 * Starts the pollthread running, which will use m's PeakHandler when a peak
	 * event is done
	 * 
	 * @param m
	 *            monitor using this thread
	 */
	PollThread(Monitor m) {
		state = PollThreadState.RUNNING;
		android.os.Process
				.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
		this.ph = m.getPeakHandler();
		start();
	}

	// TODO: Switch to using absolute threshold that gets varied depending on
	// the phone's range
	// TODO: Improve peak detection and duration detection

	@Override
	public void run() {
		short[] resultBuffer;
		int ix = 0;
		int peakIndex = -1;

		int N = AudioRecord.getMinBufferSize(44100,
				AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
		resultBuffer = new short[PEAK_DETECT_DURATION_IN_SAMPLES];
		// Only initialize an AudioRecord if it isn't initialized
		recorder = new AudioRecord(AudioSource.DEFAULT, 44100,
				AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, N);

		recorder.startRecording();
		threshold = THRESHOLD;
		// ... loop
		boolean peak = false;
		while (state != PollThreadState.STOPPED) {
			recorder.read(resultBuffer, ix, N);

			updateMonitorBars(resultBuffer, ix, N);

			int result = -1;
			if (!peak) {
				// save the buffer
				short tmp[] = getLPF(resultBuffer, ix, N);
				result = isPeakIn(tmp, 0, N);
			}
			if (result != -1) {
				ph.onPeakStarted();
				peakIndex = result;
				peak = true;
			}

			if (peak) {
				// recording, so start reading
				ix += N;
			}
			if (ix >= PEAK_DETECT_DURATION_IN_SAMPLES) {
				// TODO: Start a processing thread instead of doing it in this
				// thread
				process(resultBuffer);
				ph.peakDetected(new AudioClip(record, peakTimes));
				peak = false;
				ix = 0;
				// NOTE: We don't stop and start recording because AudioRecord
				// has a bug where stop actually releases it
			}

		}

		recorder.stop();
		recorder.release();
		recorder = null;
	}

	// The number of bytes that have been passed over for updating monitor bars
	// so far
	private int bytesPassed = 0;

	private void updateMonitorBars(short[] resultBuffer, int ix, int n) {
		bytesPassed += n;
		while (bytesPassed > AUDIO_POLL_RATE) {
			for (AudioListener al : Monitor.getMonitor().getAudioListeners())
				al.onAudioLevelReceived(resultBuffer[ix + n
						- (bytesPassed - AUDIO_POLL_RATE) - 1]);
			bytesPassed = Math.max(0, bytesPassed - n);
		}
	}

	// Process the audio and report peaks and peakEvents
	private void process(short[] resultBuffer) {
		peakTimes = new ArrayList<PeakEvent>();
		// save the chunk to record:
		record = new short[RECORD_DURATION_IN_SAMPLES];
		for (int i = 0; i < RECORD_DURATION_IN_SAMPLES; i++) {
			record[i] = resultBuffer[i];
		}

		// Apply LPF
		applyLPF(resultBuffer, 0, resultBuffer.length);

		// Detect peaks and subpeaks
		detectPeaks(resultBuffer, 0, resultBuffer.length);
	}

	private void detectPeaks(short[] buffer, int start, int length) {
		boolean inPeak = false;
		int inPeakSample = 0;
		short inPeakVolume = 0;
		short startVolume = 0;

		int i = start;
		short prev = buffer[i];
		i = 1;
		while (i < start + length) {
			short cur = buffer[i];
			if (!inPeak) {
				// Check if there is a peak starting
				if (cur - prev >= THRESHOLD) {
					inPeak = true;
					inPeakSample = i;
					inPeakVolume = cur;
					startVolume = cur;
				}
			} else {
				inPeakVolume = (short) Math.max(cur, inPeakVolume);
				// Check if a peak ended
				if (cur <= inPeakVolume / 1.1) {
					// end peak
					inPeak = false;
					// just assume, once at peak of a sound, that the sound will
					// last twice as long
					peakTimes.add(new PeakEvent(((double) inPeakSample)
							/ SAMPLE_RATE,
							(((double) inPeakVolume) / highestAudio) * 100,
							(((double) (i - inPeakSample)) / SAMPLE_RATE)));
					i += MIN_PEAK_SPACING_IN_SAMPLES;
				}
			}
			prev = cur;
			i++;
		}
		if (inPeak) {
			peakTimes.add(new PeakEvent(((double) inPeakSample) / SAMPLE_RATE,
					inPeakVolume, ((double) (i - inPeakSample)) / SAMPLE_RATE));
		}
	}

	// non-destructive lpf - return an array containing the range - 1
	private short[] getLPF(short[] buffer, int startIndex, int length) {
		short res[] = new short[length - 1];
		short val = (short) Math.abs(buffer[startIndex]);
		short curVal = 0;
		for (int i = startIndex + 1; i < startIndex + length
				&& i < buffer.length; i++) {
			curVal = (short) Math.abs(buffer[i]);
			val += (curVal - val) / SMOOTHING;
			res[i - startIndex - 1] = val;
		}
		return res;
	}

	private void applyLPF(short[] buffer, int startIndex, int length) {
		short val = (short) Math.abs(buffer[startIndex]);
		buffer[startIndex] = val;
		short curVal = 0;
		for (int i = startIndex + 1; i < startIndex + length
				&& i < buffer.length; i++) {
			curVal = (short) Math.abs(buffer[i]);
			val += (curVal - val) / SMOOTHING;
			buffer[i] = val;
			highestAudio = (short) Math.max(val, highestAudio);
		}
	}

	// Check if there is a peak. Should apply LPF before this
	// Returns -1 if no peak, index of the peak if it is in the buffer
	// Start specifies where to start looking in buffer, and length is how long
	// from start to look
	private int isPeakIn(short[] buffer, int start, int length) {
		for (int i = start + 1; i < start + length && i < buffer.length; i++) {
			if (buffer[i] - buffer[i - 1] >= threshold) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * Stop this thread in a safe way
	 */
	public void close() {
		state = PollThreadState.STOPPED;
	}

	@Override
	public void finalize() {
		close();
	}
}
